﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace OmronLib
{
    /*
     *Copyright: Copyright (c) 2019
     *Created on 2019-11-7 
     *Author: coder_li@outlook.com
     */

    /// <summary>
    /// 欧姆龙PLC使用FINS通讯，在与PLC建立tcp连接后，必须先进行读写准备，获取到DA1和SA1
    /// 这里默认读取200个D区地址，从D5000开始到D5199，读取的返回值都是高位在前低位在后
    /// 写入时都是单个寄存器写入，写入的内容也是高位在前低位在后
    /// </summary>
    public class Omron
    {

        /// <summary>
        /// PLCIP
        /// </summary>
        public IPAddress IPAddr { get; set; }

        /// <summary>
        /// PLC端口号
        /// </summary>
        public int Port { get; set; }

        /// <summary>
        /// 获取是否已经连接到PLC
        /// </summary>
        /// <returns></returns>
        public bool IsConnected
        {
            get
            {
                if (PlcClient == null)
                    return false;
                else
                    return PlcClient.Connected;
            }
        }

        /// <summary>
        /// PLC内存区域类型
        /// </summary>
        public enum AreaType
        {
            CIO_Bit = 0x30,
            WR_Bit = 0x31,
            HR_Bit = 0x32,
            AR_Bit = 0x33,
            DM_Bit = 0x02,
            CIO_Word = 0xB0,
            WR_Word = 0xB1,
            HR_Word = 0xB2,
            AR_Word = 0xB3,
            DM_Word = 0x82
        }


        private readonly object locker = new object();
        private TcpClient PlcClient = null;
        private NetworkStream Stream = null;
        private IPEndPoint PlcIP = null;

        private byte DA1 = 0x00, SA1 = 0x00;

        public Omron()
        { }

        #region 命令模板
        /// <summary>
        /// 写命令
        /// </summary>
        private readonly byte[] WriteCommand = new byte[] { 0x46, 0x49, 0x4e, 0x53, //  F   I   N   S
                                                            0x00, 0x00, 0x00, 0x1c, //  命令长度
                                                            0x00, 0x00, 0x00, 0x02, //  命令码
                                                            0x00, 0x00, 0x00, 0x00, //  错误码
                                                            0x80, 0x00, 0x02, 0x00, //  ICF RSV GCT DNA 
                                                            0x00, 0x00, 0x00, 0x00, //  DA1 DA2 SNA SA1 
                                                            0x00, 0xDA,             //  SA2 SID
                                                                        0x01, 0x02, //  FINS主命令 FINS从命令     （01 02 写）
                                                            0x82, 0x00, 0x00, 0x00, //  寄存器控制位  开始字地址(从高到低) 开始位地址
                                                            0x00, 0x01 };   //  写入数量(从高到低) 后面都是写入的内容(可以加长)
        /// <summary>
        /// 读命令
        /// </summary>
        private readonly byte[] ReadCommand = new byte[] { 0x46, 0x49, 0x4e, 0x53,  //  F   I   N   S
                                                           0x00, 0x00, 0x00, 0x1a,  //  命令长度
                                                           0x00, 0x00, 0x00, 0x02,  //  命令码
                                                           0x00, 0x00, 0x00, 0x00,  //  错误码
                                                           0x80, 0x00, 0x02, 0x00,  //  ICF RSV GCT DNA                              ***************
                                                           0x00, 0x00, 0x00, 0x00,  //  DA1 DA2 SNA SA1                                FINS Header
                                                           0x00, 0xDA,              //  SA2 SID                                      ***************
                                                                       0x01, 0x01,  //  FINS主命令 FINS从命令     （01 01 读）         ***************
                                                           0x82, 0x00, 0x00, 0x00,  //  寄存器控制位  开始字地址(高，低) 开始位地址        FINS Command
                                                           0x00, 0x01 };            //  读取数量(高，低)                               ***************
        /// <summary>
        /// 获取节点地址的命令
        /// </summary>
        private readonly byte[] PrepareCmd = new byte[] { 0x46, 0x49, 0x4e, 0x53,
                                                          0x00, 0x00, 0x00, 0x0c,
                                                          0x00, 0x00, 0x00, 0x00,
                                                          0x00, 0x00, 0x00, 0x00,
                                                          0x00, 0x00, 0x00, 0x00 };

        #endregion

        /// <summary>
        /// 向PLC发送指令
        /// </summary>
        /// <param name="commands">指令</param>
        /// <param name="info">报错信息</param>
        /// <returns>是否发送成功</returns>
        private bool DataSend(byte[] commands)
        {
            string info;
            try
            {
                if (this.PlcClient != null && PlcClient.Connected)
                {
                    //lock (locker)
                    {
                        if (Stream == null) Stream = PlcClient.GetStream();
                        Stream.Write(commands, 0, commands.Length);
                        Stream.Flush();
                    }
                    return true;
                }
                else
                {
                    info = "连接已断开";
                    return false;
                }
            }
            catch (Exception exp)
            {
                info = "发送指令到PLC失败 " + exp.Message;
                return false;
            }

        }

        /// <summary>
        /// 从PLC读取数据
        /// </summary>
        /// <param name="isExtCmd"></param>
        /// <param name="info">报错信息</param>
        /// <returns></returns>
        private string DataRead()
        {

            byte[] buffer = new byte[1024];
            int bytestoread = 0;
            string info;

            try
            {
                //if (Stream.DataAvailable)
                {
                    int start = Environment.TickCount;
                    do
                    {
                        bytestoread = Stream.Read(buffer, 0, buffer.Length);
                        if (Math.Abs(Environment.TickCount - start) >= 20000)
                        {
                            info = "PLC应答超时，请确认PLC连接线路及cpu是否有报错！";
                            break;
                        }
                    } while (bytestoread == 0);

                    if (buffer[11] == 3)
                    {
                        if (buffer[15] == 1)
                        {
                            info = "发送的命令不是有效的FINS命令！";
                            return "";
                        }
                        else if (buffer[15] == 2)
                        {
                            info = "发送的命令长度超出范围！";
                            return "";
                        }
                        else if (buffer[15] == 3)
                        {
                            info = "PLC不支持该‘FINS’命令！";
                            return "";
                        }
                    }
                    if (buffer[25] == WriteCommand[25])
                        return DataTools.ByteToHexString(buffer, bytestoread);
                    else
                    {
                        info = "PLC返回错误，返回值  " + buffer[0].ToString();
                        return "";
                    }
                }
            }
            catch
            {
                return "";
            }

        }

        /// <summary>
        /// 判断是否成功写入命令到PLC
        /// </summary>
        /// <returns></returns>
        private bool Write2PlcSuccess()
        {
            byte[] buffer = new byte[1024];
            int bytestoread = 0;
            string info;
            int start = Environment.TickCount;

            try
            {
                //lock (locker)
                {
                    do
                    {
                        bytestoread = Stream.Read(buffer, 0, buffer.Length);
                        if (Math.Abs(Environment.TickCount - start) >= 1000)
                        {
                            info = "PLC应答超时，请确认PLC连接线路及cpu是否有报错！";
                            break;
                        }
                    } while (bytestoread == 0);

                }
                if (buffer[25] == WriteCommand[25]) return true;
                else
                {
                    info = "PLC返回错误，返回值  " + buffer[1].ToString();
                    return false;
                }
            }
            catch
            {
                return false;
            }

        }

        /// <summary>
        /// 发送连接字符串，获取PLC返回的DA1和SA1值
        /// </summary>
        /// <returns></returns>
        private bool ConnectPrepare()
        {
            try
            {
                Stream.Write(PrepareCmd, 0, PrepareCmd.Length);
                byte[] res = new byte[24];
                Stream.Read(res, 0, res.Length);
                DA1 = res[23];
                SA1 = res[19];
            }
            catch { return false; }
            return true;
        }

        /// <summary>
        /// 检查命令帧中的EndCode
        /// </summary>
        /// <param name="Main">主码</param>
        /// <param name="Sub">副码</param>
        /// <param name="info">错误信息</param>
        /// <returns>指示程序是否可以继续进行</returns>
        private bool CheckEndCode(byte Main, byte Sub, out string info)
        {
            info = "";
            switch (Main)
            {
                case 0x00:
                    switch (Sub)
                    {
                        case 0x00: return true;//the only situation of success
                        case 0x01: info = "service canceled"; return false;
                    }
                    break;
                case 0x01:
                    switch (Sub)
                    {
                        case 0x01: info = "local node not in network"; return false;
                        case 0x02: info = "token timeout"; return false;
                        case 0x03: info = "retries failed"; return false;
                        case 0x04: info = "too many send frames"; return false;
                        case 0x05: info = "node address range error"; return false;
                        case 0x06: info = "node address duplication"; return false;
                    }
                    break;
                case 0x02:
                    switch (Sub)
                    {
                        case 0x01: info = "destination node not in network"; return false;
                        case 0x02: info = "unit missing"; return false;
                        case 0x03: info = "third node missing"; return false;
                        case 0x04: info = "destination node busy"; return false;
                        case 0x05: info = "response timeout"; return false;
                    }
                    break;
                case 0x03:
                    switch (Sub)
                    {
                        case 0x01: info = "communications controller error"; return false;
                        case 0x02: info = "CPU unit error"; return false;
                        case 0x03: info = "controller error"; return false;
                        case 0x04: info = "unit number error"; return false;
                    }
                    break;
                case 0x04:
                    switch (Sub)
                    {
                        case 0x01: info = "undefined command"; return false;
                        case 0x02: info = "not supported by model/version"; return false;
                    }
                    break;
                case 0x05:
                    switch (Sub)
                    {
                        case 0x01: info = "destination address setting error"; return false;
                        case 0x02: info = "no routing tables"; return false;
                        case 0x03: info = "routing table error"; return false;
                        case 0x04: info = "too many relays"; return false;
                    }
                    break;
                case 0x10:
                    switch (Sub)
                    {
                        case 0x01: info = "command too long"; return false;
                        case 0x02: info = "command too short"; return false;
                        case 0x03: info = "elements/data don't match"; return false;
                        case 0x04: info = "command format error"; return false;
                        case 0x05: info = "header error"; return false;
                    }
                    break;
                case 0x11:
                    switch (Sub)
                    {
                        case 0x01: info = "area classification missing"; return false;
                        case 0x02: info = "access size error"; return false;
                        case 0x03: info = "address range error"; return false;
                        case 0x04: info = "address range exceeded"; return false;
                        case 0x06: info = "program missing"; return false;
                        case 0x09: info = "relational error"; return false;
                        case 0x0a: info = "duplicate data access"; return false;
                        case 0x0b: info = "response too long"; return false;
                        case 0x0c: info = "parameter error"; return false;
                    }
                    break;
                case 0x20:
                    switch (Sub)
                    {
                        case 0x02: info = "protected"; return false;
                        case 0x03: info = "table missing"; return false;
                        case 0x04: info = "data missing"; return false;
                        case 0x05: info = "program missing"; return false;
                        case 0x06: info = "file missing"; return false;
                        case 0x07: info = "data mismatch"; return false;
                    }
                    break;
                case 0x21:
                    switch (Sub)
                    {
                        case 0x01: info = "read-only"; return false;
                        case 0x02: info = "protected , cannot write data link table"; return false;
                        case 0x03: info = "cannot register"; return false;
                        case 0x05: info = "program missing"; return false;
                        case 0x06: info = "file missing"; return false;
                        case 0x07: info = "file name already exists"; return false;
                        case 0x08: info = "cannot change"; return false;
                    }
                    break;
                case 0x22:
                    switch (Sub)
                    {
                        case 0x01: info = "not possible during execution"; return false;
                        case 0x02: info = "not possible while running"; return false;
                        case 0x03: info = "wrong PLC mode"; return false;
                        case 0x04: info = "wrong PLC mode"; return false;
                        case 0x05: info = "wrong PLC mode"; return false;
                        case 0x06: info = "wrong PLC mode"; return false;
                        case 0x07: info = "specified node not polling node"; return false;
                        case 0x08: info = "step cannot be executed"; return false;
                    }
                    break;
                case 0x23:
                    switch (Sub)
                    {
                        case 0x01: info = "file device missing"; return false;
                        case 0x02: info = "memory missing"; return false;
                        case 0x03: info = "clock missing"; return false;
                    }
                    break;
                case 0x24:
                    switch (Sub)
                    { case 0x01: info = "table missing"; return false; }
                    break;
                case 0x25:
                    switch (Sub)
                    {
                        case 0x02: info = "memory error"; return false;
                        case 0x03: info = "I/O setting error"; return false;
                        case 0x04: info = "too many I/O points"; return false;
                        case 0x05: info = "CPU bus error"; return false;
                        case 0x06: info = "I/O duplication"; return false;
                        case 0x07: info = "CPU bus error"; return false;
                        case 0x09: info = "SYSMAC BUS/2 error"; return false;
                        case 0x0a: info = "CPU bus unit error"; return false;
                        case 0x0d: info = "SYSMAC BUS No. duplication"; return false;
                        case 0x0f: info = "memory error"; return false;
                        case 0x10: info = "SYSMAC BUS terminator missing"; return false;
                    }
                    break;
                case 0x26:
                    switch (Sub)
                    {
                        case 0x01: info = "no protection"; return false;
                        case 0x02: info = "incorrect password"; return false;
                        case 0x04: info = "protected"; return false;
                        case 0x05: info = "service already executing"; return false;
                        case 0x06: info = "service stopped"; return false;
                        case 0x07: info = "no execution right"; return false;
                        case 0x08: info = "settings required before execution"; return false;
                        case 0x09: info = "necessary items not set"; return false;
                        case 0x0a: info = "number already defined"; return false;
                        case 0x0b: info = "error will not clear"; return false;
                    }
                    break;
                case 0x30:
                    switch (Sub)
                    { case 0x01: info = "no access right"; return false; }
                    break;
                case 0x40:
                    switch (Sub)
                    { case 0x01: info = "service aborted"; return false; }
                    break;
            }
            info = "unknown exception";
            return false;
        }

        /// <summary>
        /// PLC初始化
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        private bool PlcInit()
        {
            bool flag = false;
            PlcIP = new IPEndPoint(IPAddr, Port);
            PlcClient = new TcpClient();
            PlcClient.Connect(PlcIP);
            if (PlcClient != null)
            {
                Stream = PlcClient.GetStream();
                if (ConnectPrepare())
                {
                    flag = true;
                }
            }

            return flag;
        }

        /// <summary>
        /// 连接PLC
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        public bool PlcConnect(out string info)
        {
            info = "";
            try
            {
                if (PlcClient == null)
                {
                    PlcInit();
                }
                if (!PlcClient.Connected)   // 没连上的话重试一遍
                {
                    PlcClient.Close();
                    PlcClient = null;
                    PlcInit();
                }
                return PlcClient.Connected;
            }
            catch (Exception exp)
            {
                info = "连接PLC失败\n" + exp.Message;
                return false;
            }
        }

        /// <summary>
        /// 断开PLC连接
        /// </summary>
        /// <returns></returns>
        public bool DisConnect()
        {
            if (this.PlcClient == null)
                return true;
            if (!this.PlcClient.Connected)
                return true;

            this.PlcClient.Close();
            return true;
        }



        #region 读

        /// <summary>
        /// 读PLC字或位
        /// 读位时从左到右是低位到高位
        /// </summary>
        /// <param name="type">PLC内存区类型</param>
        /// <param name="startword">开始地址（字）</param>
        /// <param name="startbit">开始地址（位）</param>
        /// <param name="count">长度</param>
        /// <param name="result">返回的字符数组，每个元素表示一个字</param>
        /// <returns></returns>
        public bool Read(AreaType type, int startword, int startbit, int count, out string[] result)
        {
            result = new string[0];
            byte[] cmd = (byte[])ReadCommand.Clone();
            cmd[20] = DA1;        //连接时获取到的DA1
            cmd[23] = SA1;        //连接时获取到的SA1

            // 内存类型
            cmd[28] = (byte)type;

            // 读取起始地址
            byte[] bytesAddr = BitConverter.GetBytes((short)startword);   //BitConverter转出来的Byte按从低位到高位排列
            cmd[29] = bytesAddr[1];
            cmd[30] = bytesAddr[0];
            cmd[31] = (byte)startbit;

            // 读取长度
            byte[] bytesLength = BitConverter.GetBytes((short)count);
            cmd[32] = bytesLength[1];
            cmd[33] = bytesLength[0];

            //开始读取
            lock (locker)
            {
                if (DataSend(cmd))
                {
                    string res = DataRead();
                    if (res.Length == 0)
                        return false;

                    result = DataTools.StrToStrArray(res.Substring(60));
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 读取DM区的字
        /// </summary>
        /// <param name="startword"></param>
        /// <param name="count"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public bool ReadDWords(int startword, int count, out string[] result)
        {
            string[] res;
            bool isSucess = Read(AreaType.DM_Word, startword, 0, count, out res);
            result = res;
            return isSucess;
        }

        /// <summary>
        /// 读取DM区的位
        /// </summary>
        /// <param name="startword"></param>
        /// <param name="startbit"></param>
        /// <param name="count"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public bool ReadDBits(int startword, int startbit, int count, out string[] result)
        {
            string[] res;
            bool isSucess = Read(AreaType.DM_Bit, startword, startbit, count, out res);
            result = res;
            return isSucess;
        }

        #endregion

        #region 写

        /// <summary>
        /// 写PLC字
        /// </summary>
        /// <param name="type">地址类型</param>
        /// <param name="startword">开始字地址</param>
        /// <param name="count">字数</param>
        /// <param name="paras">值</param>
        /// <returns>写入成功与否</returns>
        public bool WriteWords(AreaType type, int startword, int count, int[] paras)
        {
            byte[] hexadd = BitConverter.GetBytes(startword);
            byte[] sendCmd = WriteCommand.Concat(new byte[count * 2]).ToArray();    // 这里扩容sendCmd数组，不然会溢出

            sendCmd[7] = (byte)(26 + count * 2);
            sendCmd[20] = DA1;
            sendCmd[23] = SA1;

            // 内存类型
            sendCmd[28] = (byte)type;

            // 写入起始地址
            byte[] bytesAddr = BitConverter.GetBytes((short)startword);   //BitConverter转出来的Byte按从低位到高位排列
            sendCmd[29] = bytesAddr[1];
            sendCmd[30] = bytesAddr[0];
            sendCmd[31] = 0;

            // 写入长度
            byte[] bytesLength = BitConverter.GetBytes((short)count);
            sendCmd[32] = bytesLength[1];
            sendCmd[33] = bytesLength[0];

            byte[] hexPara;
            for (int i = 0; i < count; i++)
            {
                hexPara = BitConverter.GetBytes(paras[i]);
                sendCmd[34 + i * 2] = hexPara[1];
                sendCmd[34 + i * 2 + 1] = hexPara[0];
            }

            lock (locker)
            {
                if (DataSend(sendCmd))
                {
                    if (Write2PlcSuccess())
                        return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 写PLC位
        /// 
        /// </summary>
        /// <param name="type">地址类型</param>
        /// <param name="startword">开始字地址</param>
        /// <param name="startbit">开始位地址</param>
        /// <param name="count">写入位数</param>
        /// <param name="paras">值</param>
        /// <returns>写入成功与否</returns>
        public bool WriteBits(AreaType type, int startword, int startbit, int count, bool[] paras)
        {
            byte[] hexadd = BitConverter.GetBytes(startword);
            byte[] sendCmd = WriteCommand.Concat(new byte[count]).ToArray();    // 这里扩容sendCmd数组，不然会溢出

            // 命令长度
            sendCmd[7] = (byte)(26 + count);

            sendCmd[20] = DA1;
            sendCmd[23] = SA1;

            // 内存类型
            sendCmd[28] = (byte)type;

            // 写入起始地址
            byte[] bytesAddr = BitConverter.GetBytes((short)startword);   //BitConverter转出来的Byte按从低位到高位排列
            sendCmd[29] = bytesAddr[1];
            sendCmd[30] = bytesAddr[0];
            sendCmd[31] = (byte)startbit;

            // 写入长度
            byte[] bytesLength = BitConverter.GetBytes((short)count);
            sendCmd[32] = bytesLength[1];
            sendCmd[33] = bytesLength[0];

            for (int i = 0; i < count; i++)
            {
                sendCmd[34 + i] = Convert.ToByte(paras[i]);
            }

            lock (locker)
            {
                if (DataSend(sendCmd))
                {
                    if (Write2PlcSuccess())
                        return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 写D字
        /// </summary>
        /// <param name="startword"></param>
        /// <param name="count"></param>
        /// <param name="paras"></param>
        /// <returns></returns>
        public bool WriteDWords(int startword, int count, int[] paras)
        {
            return WriteWords(AreaType.DM_Word, startword, count, paras);
        }

        /// <summary>
        /// 写D位
        /// </summary>
        /// <param name="startword"></param>
        /// <param name="startbit"></param>
        /// <param name="count"></param>
        /// <param name="paras"></param>
        /// <returns></returns>
        public bool WriteDBits(int startword, int startbit, int count, bool[] paras)
        {
            return WriteBits(AreaType.DM_Bit, startword, startbit, count, paras);
        }

        #endregion

    }
}
